import { isObject } from '../util/index.js'
import { pushTarget, popTarget } from './dep.js'
import { queueWatcher } from './scheduler.js'

// 全局变量id 每次 new Watcher 都会自增
let id = 0

/**
 * 观察者类：我们可以把 Watcher 当做观察者 需要订阅数据的变动，当数据变动后，通知它去执行某些方法
 *
 * 本质就是一个构造函数 初始化的时候会去执行 get 方法
 */
export default class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    this.exprOrFn = exprOrFn
    this.cb = cb //回调函数 比如在watcher更新之前可以执行beforeUpdate方法
    this.options = options //额外的选项 true代表渲染watcher
    this.id = id++ // watcher的唯一标识

    this.deps = [] // 存放dep的容器
    this.depsId = new Set() // 用来去重dep

    this.user = options.user // 标识用户 watcher

    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    } else {
      this.getter = function () {
        //用户watcher传过来的可能是一个字符串   类似a.a.a.a.b
        let path = exprOrFn.split('.')
        let obj = vm
        for (let i = 0; i < path.length; i++) {
          obj = obj[path[i]] //vm.a.a.a.a.b
        }
        return obj
      }
    }

    // 实例化就进行一次取值操作 进行依赖收集过程
    this.value = this.get()
  }

  get() {
    pushTarget(this) // 将 watcher 挂到 Dep.target
    // //如果watcher是渲染watcher 那么就相当于执行  vm._update(vm._render()) 这个方法在render函数执行的时候会取值 从而实现依赖收集
    const res = this.getter.call(this.vm) // exprOrFn 如果是一个函数, 则会执行
    popTarget()
    return res
  }

  // 把dep放到deps里面 同时保证同一个dep只被保存到watcher一次  同样的  同一个watcher也只会保存在dep一次
  addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
      this.depsId.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }

  // 这里简单的就执行以下get方法  之后涉及到计算属性就不一样了
  update() {
    // 每次 watcher 进行更新的时候 是否可以让他们先缓存起来 之后再一起调用
    queueWatcher(this)
  }

  run() {
    const newVal = this.get() // 新值
    const oldVal = this.value // 旧值

    this.value = newVal //现在的新值将成为下一次变化的老值

    if (this.user) {
      // 如果两次的值不相同  或者值是引用类型 因为引用类型新老值是相等的 他们是指向同一引用地址
      if (newVal !== oldVal || isObject(newVal)) {
        this.cb.call(this.vm, newVal, oldVal)
      }
    } else {
      // 渲染watcher
      this.cb.call(this.vm)
    }
  }
}
